package com.dd.seckill.cluster.config.redis.ratelimit;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Component;

import java.util.Collections;
import java.util.List;

/**
 * @Author liuxianmeng
 * @CreateTime 2025/9/14 9:22
 * @Description Redis限流器
 *
 *  特性	            固定窗口	            滑动窗口
 *  精度	            较低，有临界问题	    较高，更精确
 *  性能	            更好，操作简单	    稍差，需要清理数据
 *  内存	            占用少	            占用较多(存储时间戳)
 *  适用场景	        一般限流需求	        精确限流需求
 */
@Component
public class RedisRateLimiter {

    @Autowired
    private StringRedisTemplate redisTemplate;

    /**
     * 滑动窗口限流器工作流程
     *
     *      有序集合存储：使用Redis的ZSET存储请求时间戳
     *      动态清理：每次请求先清理窗口外的过期数据
     *      精确计数：只统计当前滑动窗口内的请求数量
     *      时间精度：毫秒级的时间精度控制
     *
     * local key = KEYS[1]              -- 限流key
     * local now = tonumber(ARGV[1])    -- 当前时间戳
     * local window = tonumber(ARGV[2]) -- 时间窗口(秒)
     * local limit = tonumber(ARGV[3])  -- 最大请求数
     *
     * local clearTime = now - window * 1000  -- 计算清理时间点
     * redis.call('zremrangebyscore', key, 0, clearTime)  -- 清理过期请求
     *
     * local count = redis.call('zcard', key)  -- 获取当前窗口内请求数
     *
     * if count < limit then
     *     redis.call('zadd', key, now, now)  -- 添加当前请求
     *     redis.call('expire', key, window + 1)  -- 设置过期时间
     *     return 1  -- 允许请求
     * else
     *     return 0  -- 拒绝请求
     * end
     *
     *
     * 当前时间: 90s
     * 滑动窗口: 30s-90s (60秒窗口)
     *
     * 时间轴: 0   10   20   30   40   50   60   70   80   90
     * 请求:   ×   ×   ×   √   √   √   √   √   √   √
     * 有效请求:           [-----------------------]
     * 计数: 7个请求(30s-90s之间)
     */
    private static final String SLIDING_WINDOW_LUA =
            "local key = KEYS[1]\n" +
            "local now = tonumber(ARGV[1])\n" +
            "local window = tonumber(ARGV[2])\n" +
            "local limit = tonumber(ARGV[3])\n" +
            "local clearTime = now - window * 1000\n" +
            "\n" +
            "redis.call('zremrangebyscore', key, 0, clearTime)\n" +
            "local count = redis.call('zcard', key)\n" +
            "\n" +
            "if count < limit then\n" +
            "    redis.call('zadd', key, now, now)\n" +
            "    redis.call('expire', key, window + 1)\n" +
            "    return 1\n" +
            "else\n" +
            "    return 0\n" +
            "end";

    /**
     * 固定窗口限流器
     */
    private static final String FIXED_WINDOW_LUA =
            "local key = KEYS[1]\n" +  // 限流key
            "local limit = tonumber(ARGV[1])\n" +  // 最大请求数
            "local window = tonumber(ARGV[2])\n" + // 时间窗口(秒)
            "\n" +
            "local current = redis.call('get', key)\n" +  // 获取当前计数
            "if current and tonumber(current) >= limit then\n" +
            "    return 0\n" +  // 超过限制，拒绝请求
            "else\n" +
            "    redis.call('incr', key)\n" +  // 增加计数
            "    if tonumber(redis.call('ttl', key)) == -1 then\n" +
            "        redis.call('expire', key, window)\n" +  // 设置过期时间
            "    end\n" +
            "    return 1\n" +  // 允许请求
            "end";

    public boolean tryAcquire(String key, int limit, int windowSec) {
        long now = System.currentTimeMillis();

        // 所有参数都转换为String类型
        List<String> keys = Collections.singletonList("rate_limit:" + key);
        String[] args = {
                String.valueOf(now),
                String.valueOf(windowSec),
                String.valueOf(limit)
        };
        Long result = redisTemplate.execute(
                new DefaultRedisScript<>(SLIDING_WINDOW_LUA, Long.class),
                keys,
                args
        );
        return result != null && result == 1;
    }
}
